using System;
using System.IO;
using System.Collections.Generic;
using MichMan.Utilities;
using System.Net;
using System.Threading;
using System.IO.IsolatedStorage;
using System.Diagnostics;
using MichMan.Utilities.Requests;

namespace MichMan.Utilities
{
    /// <summary>
    /// An already downloaded portion of a file.  This is saved in the file manager to indicate
    /// how much of the file is available.
    /// </summary>
    public class FileRange
    {
        public long Offset { get; set; }
        public long Length { get; set; }
        public long Total { get; set; }
    }

    /// <summary>
    /// Part of a downloaded file.  This is used in processing pieces of a downloading file.
    /// </summary>
    public class DownloadedFileRange
    {
        public byte[] bytes { get; set; }
        public long Offset { get; set; }
        public long Length { get; set; }
    }


    /// <summary>
    /// Wraps a stream containing downloaded data, and contains another stream for writing
    /// data as it is downloaded.  Has an extra propery "BytesAvailable" to tell you how many
    /// downloaded bytes are available for reading at the current position.
    /// </summary>
    public class StreamingStream : Stream
    {
        private Stream m_backingStreamRead;
        private Stream m_backingStreamWrite;
        ManagedFileInfo m_mfi;
        private Uri m_uri;

        public IRequest CurrentRequest
        {
            get
            {
                return m_currentRequest;
            }
        }

        private StreamingFileRequest m_currentRequest;

        private long m_totalLength = 0;
        private long m_position = 0;

        private event RunRequestCompletedEventHandler _runRequestCompleted;
        public event RunRequestCompletedEventHandler RunRequestCompleted
        {
            add
            {
                if (Completed)
                {
                    // Tell the caller it's already complete
                    value(this, null);
                }
                _runRequestCompleted += value;
            }
            remove
            {
                _runRequestCompleted -= value;
            }
        }

        public Uri Uri
        {
            get { return m_uri; }
        }

        public StreamingStream(Uri uri)
        {
            m_uri = uri;
            m_mfi = FileManager.Instance.TempFileInfo(uri.ToString());
            FileRange frs = m_mfi.Metadata.Get<FileRange>();

            if (frs != null)
            {
                m_totalLength = frs.Total;

                IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication();

                if (isoFile.FileExists(m_mfi.Path))
                {
                    m_backingStreamRead = isoFile.OpenFile(m_mfi.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
                    if (m_backingStreamRead.Length != frs.Length)
                    {
                        // Do something about this...
                    }
                }

                if (frs.Length != frs.Total)
                {
                    // We have part of the file.  Get the rest of it.
                    Debug.WriteLine("Requesting bytes {0} to {1} of file {2}.", frs.Length, frs.Total, uri);
                    m_currentRequest = new StreamingFileRequest(uri, frs.Length, frs.Total, RangeProcessor);
                }
            }
            else
            {
                Debug.WriteLine("Requesting file {0}.", uri);
                // Get the file.
                m_currentRequest = new StreamingFileRequest(uri, RangeProcessor);
            }

            if (m_currentRequest != null)
            {
                m_currentRequest.RunRequestProgressChanged += m_currentRequest_RunRequestProgressChanged;
                m_currentRequest.RunRequestCompleted += m_currentRequest_RunRequestCompleted;
                m_currentRequest.RunRequestAsync();
            }
        }

        object _fileLock = new object();
        private void RangeProcessor(DownloadedFileRange newRange)
        {
            if (m_currentRequest == null)
            {
                // We've shut down.  No more buffering.
                return;
            }

            if (m_backingStreamWrite == null)
            {
                try
                {
                    IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication();
                    if (!isoFile.DirectoryExists(Path.GetDirectoryName(m_mfi.Path)))
                    {
                        isoFile.CreateDirectory(Path.GetDirectoryName(m_mfi.Path));
                    }

                    m_backingStreamWrite = isoFile.OpenFile(m_mfi.Path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite);
                    if (m_backingStreamRead == null)
                    {
                        m_backingStreamRead = isoFile.OpenFile(m_mfi.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
                    }
                }
                catch
                {
                }
            }

            if (m_backingStreamWrite != null)
            {
                try
                {
                    m_backingStreamWrite.Write(newRange.bytes, 0, (int)newRange.Length);

                    if (m_backingStreamWrite.Length == m_totalLength)
                    {
                        Debug.WriteLine("File Received:  {0}", this.Uri);
                        m_backingStreamWrite.Close();
                    }
                    // Specify how much of the file we have.
                    m_mfi.Metadata.Put<FileRange>(new FileRange() { Offset = 0, Length = m_backingStreamRead.Length, Total = m_totalLength });
                }
                catch
                {
                }
            }
        }

        void m_currentRequest_RunRequestCompleted(object sender, RunRequestCompletedEventArgs e)
        {
            StreamingFileRequest req = sender as StreamingFileRequest;
            req.RunRequestProgressChanged -= m_currentRequest_RunRequestProgressChanged;
            req.RunRequestCompleted -= m_currentRequest_RunRequestCompleted;

            // Specify how much of the file we have.
            m_mfi.Metadata.Put<FileRange>(new FileRange() { Offset = 0, Length = m_backingStreamRead.Length, Total = m_totalLength });

            // Forward this on to anyone listening.
            if (this._runRequestCompleted != null)
            {
                _runRequestCompleted(this, e);
            }
        }

        void m_currentRequest_RunRequestProgressChanged(object sender, RunRequestProgressChangedEventArgs e)
        {
            StreamingFileRequest req = sender as StreamingFileRequest;

            // Should start all requests with FromByte == 0, otherwise we won't know the length.
            if (m_totalLength == 0 && req.FromByte == 0)
            {
                m_totalLength = req.BytesTotal;
            }
        }

        /// <summary>
        /// Reads from the current range in a loop until the request has been satisified.
        /// </summary>
        public override int Read(byte[] buffer, int offset, int count)
        {
            if (buffer == null)
            {
                throw new ArgumentNullException("buffer");
            }
            if (offset < 0)
            {
                throw new ArgumentOutOfRangeException("offset", "offset is negative");
            }
            if (count < 0)
            {
                throw new ArgumentOutOfRangeException("counts", "count is negative");
            }
            if (offset + count > buffer.Length)
            {
                throw new ArgumentException("offset", "The sum of offset and count is larger than the buffer length.");
            }

            if (BytesAvailable == 0)
            {
                // Nothing available.
                return 0;
            }

            lock (_fileLock)
            {
                m_backingStreamRead.Position = m_position;
                int r = m_backingStreamRead.Read(buffer, offset, (int)Math.Min(BytesAvailable, (long)count));
                m_position += r;
                return r;
            }
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            throw new NotSupportedException();
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            // Allow seeks anywhere in the file, but only read if data is available.
            switch (origin)
            {
                case SeekOrigin.Begin:
                    Position = offset;
                    break;
                case SeekOrigin.Current:
                    Position = Position + offset;
                    break;
                case SeekOrigin.End:
                    Position = Length + offset;
                    break;
            }

            return -1;
        }

        /// <summary>
        /// Total number of bytes available to read at the current position.
        /// This will be negative if the current position is greater than the amount downloaded.
        /// </summary>
        public long BytesAvailable
        {
            get
            {
                if (m_backingStreamRead == null)
                {
                    return 0;
                }

                try
                {
                    return m_backingStreamRead.Length - m_position;
                }
                catch
                {
                    return 0;
                }
            }
        }

        public override void SetLength(long value)
        {
            throw new NotSupportedException();
        }

        public override void Flush()
        {
            throw new NotSupportedException();
        }

        public override void Close()
        {
            if (m_currentRequest != null)
            {
                if (!m_currentRequest.IsCompleted)
                {
                    m_currentRequest.CancelAsync();
                }
                m_currentRequest.RunRequestCompleted -= this.m_currentRequest_RunRequestCompleted;
                m_currentRequest.RunRequestProgressChanged -= this.m_currentRequest_RunRequestProgressChanged;
                m_currentRequest = null;
            }

            if (m_backingStreamWrite != null)
            {
                m_backingStreamWrite.Close();
                m_backingStreamWrite = null;
            }

            if (m_backingStreamRead != null)
            {
                m_backingStreamRead.Close();
                m_backingStreamRead = null;
            }
        }

        // Stream Properties that require implementation.
        public override bool CanRead { get { return true; } }
        public override bool CanWrite { get { return false; } }
        public override bool CanSeek { get { return true; } }
        // TODO:  Override CanTimeout?  Default is false...

        public override long Length
        {
            get
            {
                if (m_totalLength == 0 && m_currentRequest != null && m_currentRequest.FromByte == 0)
                {
                    m_totalLength = m_currentRequest.BytesTotal;
                }
                return m_totalLength;
            }
        }

        // Note:  Unlike Seek, this does not change the request.
        public override long Position
        {
            get { return m_position; }
            set
            {
                if (value > Length)
                    throw new EndOfStreamException();
                if (value < 0)
                    throw new ArgumentOutOfRangeException("value");
                m_position = value;
            }
        }

        public bool Completed
        {
            get
            {
                if (m_totalLength == 0 || m_backingStreamRead == null)
                {
                    return false;
                }

                return m_totalLength == m_backingStreamRead.Length;
            }
        }
    }

    /// <summary>
    /// Data request associated with a StreamingStream.
    /// </summary>
    public class StreamingFileRequest : DataRequest<NoResponseBody>
    {
        public StreamingFileRequest(Uri uri, Action<DownloadedFileRange> rangeProcessor)
            : this(uri, 0, 0, rangeProcessor)
        {
        }

        public StreamingFileRequest(Uri uri, long fromByte, long toByte, Action<DownloadedFileRange> rangeProcessor)
        {
            AllowReadStreamBuffering = false;
            Uri = uri;
            ToByte = toByte;
            FromByte = FromByte;

            DeserializeResponse = true;
            RangeProcessor = rangeProcessor;
        }

        Action<DownloadedFileRange> RangeProcessor;
        public long FromByte { get; private set; }
        public long ToByte { get; private set; }

        object _rangeLock = new object();

        public long BytesTotal { get; set; }
        public long BytesTransferred { get; set; }
        public bool Completed { get { return BytesTransferred == BytesTotal; } }

        protected override HttpWebRequest NewWebRequest()
        {
            HttpWebRequest request = base.NewWebRequest();
            request.AllowReadStreamBuffering = AllowReadStreamBuffering;
            if (FromByte != 0)
            {
                request.Headers["Range"] = "bytes=" + FromByte + "-" + ToByte;
            }
            return request;
        }

        protected override void ProcessPartialResult(byte[] buffer, int length, int transferred, int expected)
        {
            BytesTotal = expected;
            BytesTransferred = transferred;

            DownloadedFileRange range = new DownloadedFileRange() { Offset = FromByte + transferred - length, Length = length, bytes = buffer };
            RangeProcessor(range);
        }
    }

    /* * /

    // Keeping this old code around because it deals with peices of a file...

    /// <summary>
    /// A stream that is coming off the internets in pieces.
    /// </summary>
    public class OldStreamingStream : Stream
    {
        private DownloadedFileRange[] m_ranges = new DownloadedFileRange[0];
        private Uri m_uri;
        private StreamingFileRequest m_currentRequest;

        private bool m_completed = false;
        private long m_totalLength = 0;
        private long m_position = 0;

        public OldStreamingStream(Uri uri)
        {
            m_uri = uri;
            m_currentRequest = new StreamingFileRequest(uri, RangeProcessor);
            m_currentRequest.RunRequestProgressChanged += m_currentRequest_RunRequestProgressChanged;
            m_currentRequest.RunRequestCompleted += m_currentRequest_RunRequestCompleted;
            m_currentRequest.RunRequestAsync();
        }

        object _rangeLock = new object();
        private void RangeProcessor(DownloadedFileRange newRange)
        {
            lock (_rangeLock)
            {
                // Skip the OrderBy if at all possible.
                if (m_ranges.Length != 0 && newRange.Offset > m_ranges.Last().Offset)
                {
                    m_ranges = m_ranges.Concat(new[] { newRange }).ToArray();
                }
                else
                {
                    m_ranges = m_ranges.Concat(new[] { newRange }).OrderBy(r => r.Offset).ToArray();
                }
            }
        }

        void m_currentRequest_RunRequestCompleted(object sender, RunRequestCompletedEventArgs e)
        {
            StreamingFileRequest req = sender as StreamingFileRequest;
            req.RunRequestProgressChanged -= m_currentRequest_RunRequestProgressChanged;
            req.RunRequestCompleted -= m_currentRequest_RunRequestCompleted;
        }

        /*
        // TODO:  This must be MUCH faster!
        public List<FileRange> Ranges
        {
            get
            {
                lock (_rangeLock)
                {
                    // Immutable by copy.
                    List<FileRange> ranges = new List<FileRange>();
                    ranges.AddRange(m_ranges);
                    return ranges;
                }
            }
        }
        * /
        void m_currentRequest_RunRequestProgressChanged(object sender, RunRequestProgressChangedEventArgs e)
        {
            StreamingFileRequest req = sender as StreamingFileRequest;

            // Should start all requests with FromByte == 0, otherwise we won't know the length.
            if (m_totalLength == 0 && req.FromByte == 0)
            {
                m_totalLength = req.BytesTotal;
            }
        }

        public byte this[int pos]
        {
            get
            {
                Position = pos;
                byte[] data = new byte[1];
                if (1 != Read(data, 0, 1))
                {
                    throw new ArgumentOutOfRangeException("pos");
                }
                return data[0];
            }
        }

        /// <summary>
        /// Reads from the current range in a loop until the request has been satisified.
        /// </summary>
        public override int Read(byte[] buffer, int offset, int count)
        {
            if (buffer == null)
            {
                throw new ArgumentNullException("buffer");
            }
            if (offset < 0)
            {
                throw new ArgumentOutOfRangeException("offset", "offset is negative");
            }
            if (count < 0)
            {
                throw new ArgumentOutOfRangeException("counts", "count is negative");
            }
            if (offset + count > buffer.Length)
            {
                throw new ArgumentException("offset", "The sum of offset and count is larger than the buffer length.");
            }
            int rangeslen = 0;
            
            lock(_rangeLock)
            {
                rangeslen = m_ranges.Length;
            }

            int read = 0;
            while (true)
            {
                if (offset > buffer.Length)
                {
                    break;
                }

                int curRead = 0;
                // Read whatever we can from the current range.
                lock (_rangeLock)
                {
                    curRead = ReadFromCurrentRange(m_ranges, buffer, offset, count);
                }

                // If we can't read any more, then break;
                if (curRead == 0)
                {
                    break;
                }
                // Update the offset and count based on what was just read.
                offset += curRead;
                count -= curRead;
                read += curRead;

                if (count == 0)
                {
                    // We got everything we wanted.
                    break;
                }
            }
            return read;
        }

        /// <summary>
        /// Reads from 
        /// </summary>
        /// <param name="ranges">Array of ranges sorted by offset</param>
        /// <param name="rangeOffset">Where in the ranges to start the search for the current position.</param>
        /// <param name="buffer">Buffer to write to</param>
        /// <param name="offset">Where in the buffer to write</param>
        /// <param name="count">How much to write to the buffer.</param>
        /// <returns></returns>
        int currentRange = 0;
        public int ReadFromCurrentRange(DownloadedFileRange[] ranges, byte[] buffer, int offset, int count)
        {
            if (currentRange != 0)
            {
                if (m_position < ranges[currentRange].Offset)
                {
                    currentRange = 0;
                }
            }
            // See if current position is in downloaded ranges.
            for (int i = currentRange; i < ranges.Length; i++)
            {
                if (m_position < ranges[i].Offset)
                    continue;
                if (m_position < ranges[i].Offset + ranges[i].Length)
                {
                    // Found it!
                    currentRange = i;
                    break;
                }
            }
            if (currentRange == -1)
            {
                return 0;
            }

            DownloadedFileRange curRange = ranges[currentRange];

            long bytesAvailable = (curRange.Offset + curRange.Length) - m_position;

            // Where in this range is the current position?
            long streamPosition = m_position - curRange.Offset;

            int read = Math.Min(count, (int)bytesAvailable);
            for (int i = 0; i < read; i++)
            {
                buffer[offset+i] = curRange.bytes[streamPosition + i];
            }
            m_position += read;

            return read;
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            throw new NotSupportedException();
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            // Allow seeks anywhere in the file, but only read if data is available.
            switch (origin)
            {
                case SeekOrigin.Begin:
                    Position = offset;
                    break;
                case SeekOrigin.Current:
                    Position = Position + offset;
                    break;
                case SeekOrigin.End:
                    Position = Length + offset;
                    break;
            }

            /*
            // If BytesAvailable < Total length, get more of the file.
            long bytesAvailable = BytesAvailable;
            if (bytesAvailable < m_totalLength)
            {
                if (m_currentRequest != null)
                {
                    m_currentRequest.CancelAsync();
                }
                m_currentRequest = new StreamingFileRequest(m_uri, Position + bytesAvailable, m_totalLength, RangeProcessor);
                m_currentRequest.RunRequestProgressChanged += m_currentRequest_RunRequestProgressChanged;
                m_currentRequest.RunRequestCompleted += m_currentRequest_RunRequestCompleted;
                m_currentRequest.RunRequestAsync();
            }
            * /
            return -1;
        }

        /// <summary>
        /// Total number of bytes available to read at the current position.
        /// </summary>
        public long BytesAvailable
        {
            get
            {
                long bytesAvailable = 0;
                lock (_rangeLock)
                {
                    DownloadedFileRange[] ranges = m_ranges;
                    int i;
                    for (i = 0; i < ranges.Length; i++)
                    {
                        if (m_position < ranges[i].Offset)
                            continue;
                        if (m_position < ranges[i].Offset + ranges[i].Length)
                        {
                            // Found it!
                            bytesAvailable += ranges[i].Offset + ranges[i].Length - m_position;
                            break;
                        }
                    }
                    for (int j = i + 1; j < ranges.Length; j++, i++)
                    {
                        // If the next range is contiguous, add it's bytes as well
                        if (ranges[i].Offset + ranges[i].Length == ranges[j].Offset)
                        {
                            bytesAvailable += ranges[j].Length;
                        }
                    }
                }
                return bytesAvailable;
            }
        }

        public override void SetLength(long value)
        {
            throw new NotSupportedException();
        }

        public override void Flush()
        {
            throw new NotSupportedException();
        }

        public override void Close()
        {
            if (m_currentRequest != null)
            {
                m_currentRequest.CancelAsync();
            }
        }

        // Stream Properties that require implementation.
        public override bool CanRead { get { return true; } }
        public override bool CanWrite { get { return false; } }
        public override bool CanSeek { get { return true; } }
        // TODO:  Override CanTimeout?  Default is false...

        public override long Length
        {
            get
            {
                if (m_totalLength == 0 && m_currentRequest != null && m_currentRequest.FromByte == 0)
                {
                    m_totalLength = m_currentRequest.BytesTotal;
                }
                return m_totalLength;
            }
        }

        // Note:  Unlike Seek, this does not change the request.
        public override long Position
        {
            get { return m_position; }
            set
            {
                if (value > Length)
                    throw new EndOfStreamException();
                if (value < 0)
                    throw new ArgumentOutOfRangeException("value");
                m_position = value;
            }
        }

        public bool Completed
        {
            get
            {
                return m_completed;
            }
        }
    }
  /*  */


}
